package managers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;

import messages.MessageUtils;
import messages.elevator.ElevatorStatusRequest;
import network.managers.ObserverManager;
import network.server.ModeHandlerThread;

import state.Globals;
import state.group.ElevatorStruct;
import state.group.GroupDispatcherState;
import state.group.HallCall;
import state.group.events.DeregistrationEvent;
import state.group.events.DirectionEvent;
import state.group.events.DoorEvent;
import state.group.events.FloorEvent;
import state.group.events.RegistrationEvent;

public class ElevatorManager implements Runnable {
    
    public static final int MAX_LATE_COUNT = 5;

    private static ElevatorManager manager;
    private static Thread runThread;
    
    private static ModeHandlerThread nullThread;

    public static ElevatorManager getInstance()
    {
        return manager;
    }

    public static void init(byte maxFloors)
    {
    	nullThread = new ModeHandlerThread(null, null);
        manager = new ElevatorManager(maxFloors);
        runThread = new Thread(manager, "Elevator Manager Thread");
        runThread.start();
    }

    public static void shutdown()
    {
        manager.shouldContinue = false;
        
        try {
			runThread.join();
		} catch (InterruptedException e) {
			//NOOP
		}
    }

    private GroupDispatcherState state = new GroupDispatcherState();
    private Hashtable<Byte, ElevatorStruct> tempStructs = new Hashtable<Byte, ElevatorStruct>();
    
    private Hashtable<Byte, ModeHandlerThread> elevatorThreads = new Hashtable<Byte, ModeHandlerThread>();
    private long pingStartTime;

    private class UpdateStatus
    {
        public byte id;
        public boolean hasUpdated = false;
        public int missUpdateCount = 0;
        public long updateDelay;
        
        public UpdateStatus(byte id)
        {
            this.id = id;
        }
    }
    
    private Hashtable<Byte, UpdateStatus> updateStatus = new Hashtable<Byte, UpdateStatus>();
    private boolean shouldContinue = true;

    private ElevatorManager(byte maxFloors)
    {
        //disallow instantiation outside this class
        state.maxFloors = maxFloors;
    }

    private byte lastId = 0;

    public byte getNewId()
    {
    	Byte id = null;
        synchronized (elevatorThreads)
        {
            id = ++lastId;
    
            if (!elevatorThreads.containsKey(id))
            {
                elevatorThreads.put(id, nullThread);
            }
            else
            {
            	return -1;
            }
        }
        
        synchronized (updateStatus)
        {
        	updateStatus.put(id, new UpdateStatus(id));
        }

        return id;
    }

    public boolean registerThreadHandlerForId(byte id, ModeHandlerThread thread)
    {
        synchronized (state.elevatorIPs)
        {
            
            if (!elevatorThreads.containsKey(id))
            {
                return false;
            }
    
            if (elevatorThreads.get(id) != nullThread)
            {
                return false;
            }
    
            elevatorThreads.put(id, thread);
            
            state.elevatorIPs.put(id, thread.getRemoteSocket().getInetAddress());
        
        }

        return true;
    }
    
    public boolean removeThreadHandler(ModeHandlerThread thread)
    {
        Byte id = thread.getClientId();
        
        return removeThread(id) != null;
    }
    
    public boolean removeThreadHandlerForId(byte id)
    {
        ModeHandlerThread thread = removeThread(id);
        
        if (thread != null)
        {
            thread.shutdownThread();
        
            return true;
        }
        
        return false;
    }
    
    private ModeHandlerThread removeThread(byte id)
    {
        ModeHandlerThread thread = null;
        
        synchronized (state.elevatorIPs)
        {
            if (!elevatorThreads.containsKey(id))
            {
                return null;
            }
            
            thread = elevatorThreads.remove(id);
            
            state.elevatorIPs.remove(id);
        }
        
        synchronized (updateStatus)
        {
            updateStatus.remove(id);
        }
        
        synchronized (state.elevatorData)
        {
            if (state.elevatorData.contains(id))
            {
                state.elevatorData.remove(id);
            }
        }
        
        DeregistrationEvent event = new DeregistrationEvent(id);
        ObserverManager.getInstance().postEvent(event);
        
        return thread;
    }

    public boolean updateStatus(ModeHandlerThread thread, ElevatorStruct status)
    {
        ModeHandlerThread properThread = null;
        
        synchronized (state.elevatorIPs)
        {
            properThread = elevatorThreads.get(status.identifier);
        }

        if (properThread != thread)
        {
            return false;
        }
        
        synchronized (this.tempStructs)
        {
            tempStructs.put(status.identifier, status);
        }

        synchronized (this.updateStatus)
        {
            UpdateStatus stat = updateStatus.get(status.identifier);
            stat.hasUpdated = true;
            
            if (stat.missUpdateCount == 0)
            {
                stat.updateDelay = System.currentTimeMillis() - pingStartTime;
            }
            else
            {
                stat.updateDelay += System.currentTimeMillis() - pingStartTime;
            }
            
        }

        return true;
    }
    
    public GroupDispatcherState getDispatcherState()
    {
        return state;
    }
    
    public Hashtable<Byte, ModeHandlerThread> getElevatorThreads()
    {
        return elevatorThreads;
    }

    @Override
    public void run()
    {
        while (shouldContinue)
        {
            try
            {
                Thread.sleep(500);
            }
            catch (InterruptedException e)
            {
                //NOOP
            }
            
            if (!shouldContinue)
            {
                break;
            }

            resetStatus();
            
            this.pingStartTime = System.currentTimeMillis();
            
            sendStatusRequests();

            try
            {
                Thread.sleep(15);
            }
            catch (InterruptedException e)
            {
                //NOOP
            }
            
            if (!shouldContinue)
            {
                break;
            }

            checkStatus();
            
            checkForEvents();
            
            synchronized (state.elevatorData)
            {
                synchronized (tempStructs)
                {
                    state.elevatorData.putAll(tempStructs);
                    tempStructs.clear();
                }
            }
            
            //This will trigger the HallCallManager to assign/revoke Hall Calls based on the current state of the system.
            synchronized(HallCallManager.getInstance())
            {
                HallCallManager.getInstance().notifyAll();
            }
            
            //This will tell the ObserverManager to send out the events that have been recorded so far.
            synchronized(ObserverManager.getInstance())
            {
                ObserverManager.getInstance().notifyAll();
            }
        }
        
        for (ModeHandlerThread t : elevatorThreads.values())
        {
        	t.shutdownThread();
        }
        
    }
    
    private void resetStatus()
    {
        synchronized (updateStatus)
        {
            for (UpdateStatus stat : updateStatus.values())
            {
                stat.hasUpdated = false;
            }
        }
    }
    
    private void sendStatusRequests()
    {
        
        ElevatorStatusRequest req = new ElevatorStatusRequest();
        byte[] data = req.toByteArray();
        
        if (Globals.isDebug())
        {
        	Globals.getLogger().log("Attempting to send out Data: " + MessageUtils.byteArrayToString(data));
        }
        
        //NOTE:  may need to fork this for performance reasons!
        
        Hashtable<Byte, ModeHandlerThread> tempCopyOfThreads;
        synchronized (state.elevatorIPs)
        {
            tempCopyOfThreads = new Hashtable<Byte, ModeHandlerThread>(elevatorThreads);
        }
        
        for (ModeHandlerThread t : tempCopyOfThreads.values())
        {
            try
            {
            	if (t != nullThread && !t.getRemoteSocket().isClosed())
            	{
            		t.getRemoteSocket().getOutputStream().write(data);
            	}
            }
            catch (IOException e)
            {
                Globals.getLogger().error("Error occurred while attempted to send out the status requests", e);
            }
        }
    }
    
    private void checkStatus()
    {
        synchronized (updateStatus)
        {
        	ArrayList<UpdateStatus> toRemove = new ArrayList<UpdateStatus>();
        	
            for (UpdateStatus status : updateStatus.values())
            {
                if (!status.hasUpdated)
                {
                	status.missUpdateCount++;
                    if (status.missUpdateCount == MAX_LATE_COUNT)
                    {
                        toRemove.add(status);
                    }
                    else if (status.missUpdateCount == 1)
                    {
                        status.updateDelay = System.currentTimeMillis() - this.pingStartTime;
                    }
                    else
                    {
                        status.updateDelay += System.currentTimeMillis() - this.pingStartTime;
                    }
                }
                else
                {
                    status.missUpdateCount = 0;
                }
            }
            
            for (UpdateStatus status : toRemove)
            {
            	this.removeThreadHandlerForId(status.id);
            }
        }
    }
    
    private void checkForEvents()
    {
        synchronized (state.elevatorData)
        {
            synchronized (tempStructs)
            {
                for (Byte b : tempStructs.keySet())
                {
                    ElevatorStruct temp = tempStructs.get(b);
                    if (!state.elevatorData.containsKey(b))
                    {
                        RegistrationEvent event = new RegistrationEvent(temp.identifier, temp.doorOpen, temp.lastKnownFloor, temp.direction);
                        ObserverManager.getInstance().postEvent(event);
                    }
                    else
                    {
                        ElevatorStruct old = state.elevatorData.get(b);
                        
                        if (old.direction != temp.direction)
                        {
                            DirectionEvent event = new DirectionEvent(temp.identifier, temp.direction);
                            ObserverManager.getInstance().postEvent(event);
                        }
                        
                        if (old.lastKnownFloor != temp.lastKnownFloor)
                        {
                            FloorEvent event = new FloorEvent(temp.identifier, temp.lastKnownFloor);
                            ObserverManager.getInstance().postEvent(event);
                        }
                        
                        if (old.doorOpen != temp.doorOpen)
                        {
                            DoorEvent event = new DoorEvent(temp.identifier, temp.doorOpen);
                            ObserverManager.getInstance().postEvent(event);
                            
                            if (temp.doorOpen == 1)
                            {
                            	HallCall call = new HallCall(temp.lastKnownFloor, temp.direction);
                            	HallCallManager.getInstance().resolveHallCall(call);
                            }
                        }
                    }
                }
            }
        }
    }
}
